﻿using System;
using System.Collections.Generic;
using System.Text;
using System.Windows.Forms;
using System.Threading;
using System.IO;
using System.Diagnostics;
using Ionic.Zip;
namespace MinecraftBackupAssistant
{
    struct BackupInformation
    {
        public string World;
        public bool ShouldMap;
        public string BackupPath;
        public string BackupName;
        public string Description;
        public string DateTimeFormat;
        public string MapperPath;
        public MapperSettings MapperSettings;
        public int NumberOfBackupsToKeep;
        public bool ShouldZip;

        public BackupInformation(bool shouldZip, string world, bool shouldMap, string backupPath, string backupName,
            string description, string dateTimeFormat, string mapperPath, MapperSettings mapperSettings,
            int numberOfBackupsToKeep)
        {
            ShouldZip = shouldZip;
            World = world;
            ShouldMap = shouldMap;
            BackupPath = backupPath;
            BackupName = backupName;
            Description = description;
            DateTimeFormat = dateTimeFormat;
            MapperPath = mapperPath;
            MapperSettings = mapperSettings;
            NumberOfBackupsToKeep = numberOfBackupsToKeep;
        }
    }

    class Backup
    {
        public BackupInformation BI;
        public Action<int, int> UpdateProgress; //maximum, value

        public delegate object InvokeDelegate(Delegate del, params object[] args);

        public InvokeDelegate InvokeForm;

        public Action<string> OnFinish;
        public Action<string> MessageOutput;
        public Action<Exception> OnCriticalError;
        public Action<Exception> OnMinorError;

        public bool Threaded;

		private Thread mapperThread = null, zipThread = null, copyThread = null;
		private string mapperLog;
		private string mapperOutputPath;
		
        private string destFolder = "";
        private string sourceFolder = "";

        private int totalFilesInDirectory = 0, totalEntriesInDirectory = 0;

        public Backup(BackupInformation bi, Action<int, int> updateProgress, InvokeDelegate invoke, Action<string> onFinish, Action<Exception> onCriticalError, Action<Exception> onMinorError, Action<string> messageOutput, bool threaded)
        {
            InvokeForm += invoke;
            BI = bi;
            UpdateProgress = updateProgress;
            OnFinish = onFinish;
            OnCriticalError = onCriticalError;
            OnMinorError = onMinorError;
            MessageOutput = messageOutput;
            Threaded = threaded;
        }

        public void BackupWorld()
        {
            ThreadStart preparingAction = (() =>
            {
                try
                {
                    //string worldsPath = Path.Combine(BI.MinecraftFolder, "saves");
                    if (BI.Description != "") BI.Description = "." + BI.Description;
                    //destFolder = Path.Combine(BI.BackupPath, String.Format(new DirectoryInfo(BI.World).Name + "_{0:" + BI.DateTimeFormat + "}", DateTime.Now).Replace('/', '-').Replace(':', '-') + BI.Description);
                    destFolder = Path.Combine(BI.BackupPath, String.Format(BI.BackupName + "_{0:" + BI.DateTimeFormat + "}", DateTime.Now).Replace('/', '-').Replace(':', '-') + BI.Description);
                    sourceFolder = BI.World;


                    if (Utility.IsDirectoryEmpty(sourceFolder))
                    {
                        Exception exception = new ArgumentException(BI.World + " is empty!");
                        if (Threaded && InvokeForm != null)
                        {
                            InvokeForm(OnMinorError, exception);

                            if (OnFinish != null)
                                InvokeForm(OnFinish, BI.World + " is empty!");
                        }
                        else
                        {
                            OnMinorError(exception);

                            if (OnFinish != null)
                                OnFinish(BI.World + " is empty!");
                        }
                        return;
                    }

                    totalFilesInDirectory = Directory.GetFiles(sourceFolder, "*.*", SearchOption.AllDirectories).Length;
                    totalEntriesInDirectory = totalFilesInDirectory + Directory.GetDirectories(sourceFolder, "*.*", SearchOption.AllDirectories).Length;

                    if (BI.ShouldMap)
                    {
                        ThreadStart action = (() =>
                        {
                            Process mapper = new Process();
                            mapper.StartInfo.FileName = BI.MapperPath;
                            mapper.StartInfo.Arguments = BI.MapperSettings.GenerateMapperParameterString(sourceFolder);
							mapper.StartInfo.WorkingDirectory = Utility.TemporaryMapperPath;
							mapper.StartInfo.CreateNoWindow = true;

							mapperOutputPath = Utility.CombinePath(mapper.StartInfo.WorkingDirectory, "output.png");

							mapper.StartInfo.UseShellExecute = false;
							mapper.StartInfo.RedirectStandardOutput = true;

							mapper.Start();
							while (mapper.HasExited == false)
							{
								mapperLog = mapper.StandardOutput.ReadToEnd(); //necessary to prevent the thread from getting stuck when enabling the Render Caves
								Thread.Sleep(500);
							}

							mapperLog = mapper.StandardOutput.ReadToEnd();

							if (mapper.ExitCode != 0)
								if (OnCriticalError != null)
								{
									Exception ex = new Exception("Mapper process exited with error code " + mapper.ExitCode + "\n\nLog:\n" + mapperLog);
									if (InvokeForm != null)
										InvokeForm(OnCriticalError, ex);
									else
										OnCriticalError(ex);
								}
                        });

                        if (Threaded)
                        {
                            mapperThread = new Thread(action);
                            //mapperThread.IsBackground = true;
                            mapperThread.Start();
                        }
                        else
                            action();
                    }
                    if (Threaded && this.InvokeForm != null)
                        this.InvokeForm(new Action(beginBackup));
                    else
                        beginBackup();
                }
                catch (Exception exception)
                {
					if (OnCriticalError != null)
						if (Threaded && InvokeForm != null)
							InvokeForm(OnCriticalError, exception);
						else
							OnCriticalError(exception);
					else //we can't handle the exception
						throw exception;
                }
            });

            if (Threaded)
                new Thread(preparingAction).Start();
            else
                preparingAction();
        }

        private void beginBackup()
        {
            if (!BI.ShouldZip)
            {
                MessageOutput("Copying files...");
                ThreadStart action = (() =>
                {
                    try
                    {
                        int count = 0;
                        DateTime now = DateTime.Now;
                        SimpleFileCopy.CopyDirectoryWithProgress(new DirectoryInfo(sourceFolder), destFolder, delegate(double percentage)
                        {
                            int percent = (int)(percentage * 100);
                            if (UpdateProgress != null)
                                if (Threaded && InvokeForm != null)
                                    InvokeForm(UpdateProgress, 100, percent);
                                else
                                    UpdateProgress(100, percent);

                            if (++count % 99 == 0)
                            {
                                MessageOutput(String.Format("Copying files... ({0}/{1})", count, totalFilesInDirectory));
                            }
                        });

                        if (BI.ShouldMap && mapperThread != null && mapperThread.IsAlive)
                        {
                            MessageOutput("Creating mapper image... (this can take a while depending on your world's size)");
							while (mapperThread.IsAlive) ;
                            MessageOutput("Copying mapper image to backup...");
                            if (File.Exists(mapperOutputPath))
                            {
                                new FileInfo(mapperOutputPath).CopyTo(Utility.CombinePath(destFolder, new DirectoryInfo(destFolder).Name + Utility.MapperExtension));
                                File.Delete(mapperOutputPath);
                            }
                            else
                            {
								throw new Exception("Mapper process failed because of an unknown reason. (Could not find output file)\n\nLog:\n" + mapperLog);
                            }
                        }
                    }
                    catch (Exception exception)
                    {
						if (OnCriticalError != null)
							if (Threaded && InvokeForm != null)
								InvokeForm(OnCriticalError, exception);
							else
								OnCriticalError(exception);
						else //we can't handle the exception
							throw exception;
                    }
                    finally
                    {
                        if (Threaded && InvokeForm != null)
							InvokeForm(new Action<string, Action<string>>(deleteOldBackups), "Backup for " + new DirectoryInfo(sourceFolder).Name + " has been saved succesfully.", MessageOutput);
                        else
                            deleteOldBackups("Backup for " + new DirectoryInfo(sourceFolder).Name + " has been saved succesfully.", MessageOutput);
                    }
                });

                if (Threaded)
                {
                    copyThread = new Thread(action);
                    copyThread.Start();
                }
                else
                    action();
            }
            else
            {
                using (ZipFile zip = new ZipFile())
                {
                    ThreadStart action = (() =>
                    {
                        try
                        {
                            zip.CompressionLevel = Ionic.Zlib.CompressionLevel.Default;
                            zip.Comment = Utility.ZipComment;
                            zip.AddProgress += ((sender, e) =>
                            {
                                if (e.EventType == ZipProgressEventType.Adding_Started)
                                {
                                }
                                if (e.EventType == ZipProgressEventType.Adding_AfterAddEntry)
                                {
                                    int percent = (int)((double)e.EntriesTotal / (double)(totalEntriesInDirectory) * 100);

                                    if (UpdateProgress != null)
                                        if (Threaded)
                                            InvokeForm(UpdateProgress, 100, percent);
                                        else
                                            UpdateProgress(100, percent);

                                    if (e.EntriesTotal % 99 == 0)
                                    {
                                        MessageOutput(String.Format("Preparing files... ({0}/{1})", e.EntriesTotal, totalEntriesInDirectory));
                                    }
                                }

                            });
                            DateTime now = DateTime.Now;

                            MessageOutput("Preparing files...");

                            zip.AddDirectory(sourceFolder);

                            zip.SaveProgress += ((sender, e) =>
                            {
                                if (e.EventType == ZipProgressEventType.Saving_AfterWriteEntry)
                                {
                                    int percent = (int)((double)e.EntriesSaved / (double)(e.EntriesTotal + 1) * 100);
                                    if (UpdateProgress != null)
                                        if (Threaded)
                                            InvokeForm(UpdateProgress, 100, percent);
                                        else
                                            UpdateProgress(100, percent);

                                    if (e.EntriesSaved % 99 == 0)
                                    {
                                        MessageOutput(String.Format("Writing files... ({0}/{1})", e.EntriesSaved, e.EntriesTotal + 1));
                                    }
                                }
                                else if (e.EventType == ZipProgressEventType.Saving_Completed)
                                {

                                }
                            });

                            MessageOutput("Writing files...");
                            zip.Save(destFolder + ".zip");

                            if (mapperThread != null && BI.ShouldMap)
                            {
                                MessageOutput("Creating mapper image... (this can take a while depending on your world's size)");
								while (mapperThread.IsAlive) ;
                                MessageOutput("Adding mapper image to zip...");

                                if (File.Exists(mapperOutputPath))
                                {
                                    ZipFile zf = ZipFile.Read(destFolder + ".zip");

									FileInfo fi = new FileInfo(mapperOutputPath);
									fi.MoveTo(Utility.CombinePath(Utility.TemporaryMapperPath, new DirectoryInfo(destFolder).Name + Utility.MapperExtension));

									zf.AddFile(fi.FullName, "\\");
                                    zf.Save();
									File.Delete(mapperOutputPath);
                                }
                                else
                                {
									throw new Exception("Mapper process failed for an unknown reason. (Could not find output file)\n\nLog:\n" + mapperLog);
								}
                            }
                        }
                        catch (Exception exception)
                        {
							if (OnCriticalError != null)
								if (Threaded && InvokeForm != null)
									InvokeForm(OnCriticalError, exception);
								else
									OnCriticalError(exception);
							else //we can't handle the exception
								throw exception;
                        }
                        finally
                        {
                            if (Threaded && InvokeForm != null)
                                InvokeForm(new Action<string, Action<string>>(deleteOldBackups), "ZIP backup for " + new DirectoryInfo(sourceFolder).Name + " has been saved succesfully.", MessageOutput);
                            else
                                deleteOldBackups("ZIP backup for " + new DirectoryInfo(sourceFolder).Name + " has been saved succesfully.", MessageOutput);
                        }
                    });
                    if (Threaded)
                    {
                        zipThread = new Thread(action);
                        zipThread.Start();
                    }
                    else
                        action();
                }
            }
        }
        private void deleteOldBackups(string endText, Action<string> textOutput)
        {
            if (BI.NumberOfBackupsToKeep != 0)
            {
                textOutput("Deleting old backups...");
                ThreadStart action = (() =>
                {
                    try
                    {
                        List<BackupEntry> files = new List<BackupEntry>();
                        List<BackupEntry> directories = new List<BackupEntry>();
                        foreach (var file in Directory.GetFiles(BI.BackupPath))
                        {
                            if (Utility.NameRegex.IsMatch(Path.GetFileName(file)))
                                files.Add(new BackupEntry(file, new FileInfo(file).CreationTime, true));
                        }

                        foreach (var dir in Directory.GetDirectories(BI.BackupPath))
                        {
                            if (Utility.NameRegex.IsMatch(new DirectoryInfo(dir).Name))
                                directories.Add(new BackupEntry(dir, new DirectoryInfo(dir).CreationTime, false));
                        }

                        List<BackupEntry> total = new List<BackupEntry>(files.Count + directories.Count);

                        total.AddRange(files);
                        total.AddRange(directories);

                        total.Sort();
                        if (total.Count > BI.NumberOfBackupsToKeep)
                        {
                            for (int i = 0; i < total.Count - BI.NumberOfBackupsToKeep; i++)
                            {
                                if (total[i].IsZip == true)
                                    File.Delete(total[i].Name);
                                else
                                    Utility.DeleteDirectory(total[i].Name);

                                textOutput(String.Format("Deleting old backups... ({0}/{1})", i + 1, total.Count - BI.NumberOfBackupsToKeep));
                                int percent = (int)((double)i / (double)(total.Count - BI.NumberOfBackupsToKeep) * 100);

                                if (UpdateProgress != null)
                                    if (Threaded)
                                        InvokeForm(UpdateProgress, 100, percent);
                                    else
                                        UpdateProgress(100, percent);
                            }
                        }
                    }
                    catch (Exception exception)
                    {
						if (Threaded && InvokeForm != null)
							InvokeForm(OnCriticalError, exception);
						else
							OnCriticalError(exception);
                    }
                    finally
                    {
						if (Threaded && InvokeForm != null && OnFinish != null)
							InvokeForm(OnFinish, endText);
						else
							if (OnFinish != null)
								OnFinish(endText);
                    }
                });

                if (Threaded)
                {
                    Thread thread = new Thread(action);
                    thread.IsBackground = true;
                    thread.Start();
                }
                else
                    action();

                return;
            }
            if (Threaded && InvokeForm != null && OnFinish != null)
                InvokeForm(OnFinish, endText);
            else if (OnFinish != null)
                OnFinish(endText);
        }
    }
}
